﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="WeakEventManager.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// <summary>
//   Defines the WeakEventManager type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using Collections;

    public class WeakEventManager
    {
        #region Fields

        private readonly EventSourceMap _eventSourceMap = new EventSourceMap();
        private readonly object _lock = new object(); 

        #endregion
        
        private void RemoveSource(EventSource source)
        {
            lock (_lock)
            {
                _eventSourceMap.Remove(source);
            }
        }

        public void AddHandler<TPublisher, TDelegate>(TPublisher publisher, IEventBinder<TPublisher, TDelegate> binder,
            TDelegate handler) where TDelegate : class where TPublisher : class
        {
            var eventSource = new EventSource(publisher, binder);
            
            lock (_lock)
            {
                var entry = GetOrCreateEntry<TPublisher, TDelegate>(eventSource);
                entry.Add(handler);
            }

            // Keep the publisher alive until we have added the subscription
            GC.KeepAlive(publisher);
        }

        public void RemoveHandler<TPublisher, TDelegate>(TPublisher publisher,
            IEventBinder<TPublisher, TDelegate> binder, TDelegate handler)
            where TDelegate : class
            where TPublisher : class
        {
            var eventSource = new EventSource(publisher, binder);

            lock (_lock)
            {
                var entry = GetEntry<TPublisher, TDelegate>(eventSource);
                if (entry != null)
                {
                    entry.Remove(handler);
                }
            }
        }

        public void Flush()
        {
            lock (_lock)
            {
                _eventSourceMap.Flush();
            }
        }

        internal WeakDelegate<TDelegate> GetEvent<TPublisher, TDelegate>(object publisher, IEventBinder<TPublisher, TDelegate> binder)
            where TDelegate : class where TPublisher : class
        {
            lock (_lock)
            {
                var entry = GetEntry<TPublisher, TDelegate>(new EventSource(publisher, binder));
                return entry != null ? entry.WeakDelegate : null;
            }
        }

        private WeakEntry<TPublisher, TDelegate> GetOrCreateEntry<TPublisher, TDelegate>(EventSource eventSource)
            where TPublisher : class
            where TDelegate : class
        {
            var result = GetEntry<TPublisher, TDelegate>(eventSource);
            if (result == null)
            {
                result = new WeakEntry<TPublisher, TDelegate>(this, eventSource);
                _eventSourceMap.Add(eventSource, result);
            }

            return result;
        }

        private WeakEntry<TPublisher, TDelegate> GetEntry<TPublisher, TDelegate>(EventSource eventSource)
            where TPublisher : class where TDelegate : class
        {
            IWeakObject value;
            if (_eventSourceMap.TryGetValue(eventSource, out value))
            {
                return (WeakEntry<TPublisher, TDelegate>)value;
            }
            return null;
        }

        #region Nested type: EventSource

        internal class EventSource : IEquatable<EventSource>
        {
            #region Fields

            private readonly IEventBinder _binder;
            private readonly int _hashCode;
            private readonly WeakReference _publisherReference;

            #endregion

            internal static IEnumerable<EventSource> SelectFrom(WeakEventManager weakEventManager)
            {
                lock (weakEventManager._lock)
                {
                    return weakEventManager._eventSourceMap.Keys.ToArray();
                }
            }

            /// <summary>
            ///   Initializes a new instance of the <see cref = "EventSource" /> class.
            /// </summary>
            /// <param name = "publisher">The publisher.</param>
            /// <param name = "binder">The binder.</param>
            public EventSource(object publisher, IEventBinder binder)
            {
                _publisherReference = new WeakReference(publisher);
                _binder = binder;

                unchecked
                {
                    _hashCode = (RuntimeHelpers.GetHashCode(Publisher) * 397) ^ _binder.GetHashCode();
                }
            }

            /// <summary>
            ///   Gets the event publisher.
            /// </summary>
            /// <value>The event publisher.</value>
            public object Publisher
            {
                get { return _publisherReference.Target; }
            }

            /// <summary>
            ///   Gets the event binder.
            /// </summary>
            /// <value>The event binder.</value>
            public IEventBinder Binder
            {
                get { return _binder; }
            }

            /// <summary>
            ///   Gets a value indicating whether this instance is alive.
            /// </summary>
            /// <value><c>true</c> if this instance is alive; otherwise, <c>false</c>.</value>
            public bool IsAlive
            {
                get { return _publisherReference.IsAlive; }
            }

            #region IEquatable<EventSource> Members

            public bool Equals(EventSource other)
            {
                if (ReferenceEquals(null, other))
                {
                    return false;
                }
                if (ReferenceEquals(this, other))
                {
                    return true;
                }

                return ReferenceEquals(other.Publisher, Publisher) &&
                    Equals(other._binder, _binder);
            }

            #endregion

            public override bool Equals(object obj)
            {
                return Equals(obj as EventSource);
            }

            public override int GetHashCode()
            {
                return _hashCode;
            }
        }

        #endregion

        #region Nested type: EventSourceMap

        private class EventSourceMap : WeakKeyHashMap<EventSource, IWeakObject>
        {
            protected override sealed Entry NewEntry(EventSource key, IWeakObject value)
            {
                return new WeakDelegateMapEntry(key, value);
            }

            #region Nested type: WeakDelegateMapEntry

            private class WeakDelegateMapEntry : Entry
            {
                #region Fields

                private readonly EventSource _key;
                private IWeakObject _value;

                #endregion

                public WeakDelegateMapEntry(EventSource key, IWeakObject value)
                {
                    _key = key;
                    _value = value;
                }

                public override bool IsAlive
                {
                    get { return Key.IsAlive && Value != null && Value.QueryIsAlive(true); }
                }

                public override EventSource Key
                {
                    get { return _key; }
                }

                public override IWeakObject Value
                {
                    get { return _value; }
                    set { _value = value; }
                }
            }

            #endregion
        }

        #endregion

        #region Nested type: WeakEntry

        private class WeakEntry<TPublisher, TAction> : IDelegatable, IWeakObject
            where TAction : class
            where TPublisher : class
        {
            #region Fields

            private readonly EventSource _source;
            private readonly object _subscriptionToken;
            private TAction _invoke;

            #endregion

            public WeakEntry(WeakEventManager owner, EventSource source)
            {
                _source = source;
                _subscriptionToken = _source.Binder.Add(source.Publisher, (Delegate)(object)Invoke);
                Owner = owner;
            }

            private WeakEventManager Owner { get; set; }

            private TPublisher Publisher
            {
                get { return (TPublisher)_source.Publisher; }
            }

            private IEventBinder EventBinder
            {
                get { return _source.Binder; }
            }

            private TAction Invoke
            {
                get { return _invoke ?? (_invoke = WeakDelegateRuntime.Binder<TAction>.Bind(this)); }
            }

            internal WeakDelegate<TAction> WeakDelegate { get; private set; }

            #region IDelegatable Members

            void IDelegatable.InvokeAction()
            {
                InvokeAction(a => a.InvokeAction());
            }

            void IDelegatable.InvokeAction<TArg1>(TArg1 arg1)
            {
                InvokeAction(a => a.InvokeAction(arg1));
            }

            void IDelegatable.InvokeAction<TArg1, TArg2>(TArg1 arg1, TArg2 arg2)
            {
                InvokeAction(a => a.InvokeAction(arg1, arg2));
            }

            void IDelegatable.InvokeAction<TArg1, TArg2, TArg3>(TArg1 arg1, TArg2 arg2, TArg3 arg3)
            {
                InvokeAction(a => a.InvokeAction(arg1, arg2, arg3));
            }

            void IDelegatable.InvokeAction<TArg1, TArg2, TArg3, TArg4>(TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4)
            {
                InvokeAction(a => a.InvokeAction(arg1, arg2, arg3, arg4));
            }

            public TReturn InvokeFunc<TReturn>()
            {
                throw new NotImplementedException();
            }

            public TReturn InvokeFunc<TArg1, TReturn>(TArg1 arg1)
            {
                throw new NotImplementedException();
            }

            public TReturn InvokeFunc<TArg1, TArg2, TReturn>(TArg1 arg1, TArg2 arg2)
            {
                throw new NotImplementedException();
            }

            public TReturn InvokeFunc<TArg1, TArg2, TArg3, TReturn>(TArg1 arg1, TArg2 arg2, TArg3 arg3)
            {
                throw new NotImplementedException();
            }

            public TReturn InvokeFunc<TArg1, TArg2, TArg3, TArg4, TReturn>(TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4)
            {
                throw new NotImplementedException();
            }

            #endregion

            #region IWeakObject Members

            public bool QueryIsAlive(bool flush)
            {
                var weakDelegate = WeakDelegate;
                return _source.IsAlive && weakDelegate  != null && weakDelegate.QueryIsAlive(flush);
            }

            #endregion

            private void OnRequeryIsAliveSuggested()
            {
                // still alive?
                if (QueryIsAlive(false))
                {
                    return;
                }

                // get a copy of owner as another thread could jump in an clear it, doesn't matter if we detach twice
                var owner = Owner;
                if (owner != null)
                {
                    lock (owner._lock)
                    {
                        if (QueryIsAlive(false))
                        {
                            return;
                        }

                        Detach();
                    }
                }
            }
            
            private void Detach()
            {
                if (Owner != null)
                {
                    var publisher = Publisher;
                    if (publisher != null)
                    {
                        EventBinder.Remove(publisher, _subscriptionToken, (Delegate)(object)Invoke);
                    }

                    Owner.RemoveSource(_source);
                    Owner = null;
                }
            }
        
            private void InvokeAction(Action<IDelegatable> action)
            {
                var weakDelegate = WeakDelegate;
                if (weakDelegate != null)
                {
                    action(weakDelegate);
                    OnRequeryIsAliveSuggested();
                }
            }

            public void Add(TAction action)
            {
                lock (this)
                {
                    WeakDelegate += action;
                }
            }

            public void Remove(TAction action)
            {
                lock (this)
                {
                    WeakDelegate -= action;
                }
                
                OnRequeryIsAliveSuggested();
            }
        }

        #endregion
    }
}